package com.nado.extensions.util;

import org.junit.Assert;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 간단한 fail 테스트를 수행한다.
 */
public class TestTemplate {

	/**
	 * 간단한 fail 테스트를 수행한다.
	 *
	 * @param callable 테스트를 수행하는 실제 callable 객체
	 * @param classes  Exception 클래스(nested)
	 * @throws AssertionError 지정한 Exception이 발생하지 않았거나, 종류가 다른 경우
	 */
	public static <T> void failure(Callable<T> callable, Class<? extends Exception>... classes) throws AssertionError {
		failure(classes).test(callable);
	}

	public static Fail failure(Class<? extends Exception>... classes) {
		return new Fail(classes);
	}

	public static class Fail {

		private Class<? extends Exception>[] classes;

		private Fail(Class<? extends Exception>... classes) {
			this.classes = classes;
		}

		public <T extends Callable<?>> void test(T callable) {
			try {
				callable.call();
			} catch (Throwable t) {
				Throwable current = t;
				for (Class<? extends Exception> clazz : classes) {
					if (!clazz.isInstance(current)) {
						Assert.fail("not expected exception " + t);
					}
					current = current.getCause();
				}
				return;
			}
			Assert.fail("exception is not thrown");
		}
	}

	public static Performance performance(int thread) {
		return new Performance(thread);
	}

	public static Performance performance(Callable<?> callable, int thread) {
		return performance(thread).task(callable);
	}

	public static class Performance implements Runnable {

		private static final long DEFAULT_WARMUP = 2000;
		private static final long DEFAULT_INTERVAL = -1;

		private long warmup = DEFAULT_WARMUP;
		private long interval = DEFAULT_INTERVAL;
		private Executor executor;

		private AtomicInteger success = new AtomicInteger();
		private AtomicInteger failed = new AtomicInteger();
		private AtomicBoolean complete = new AtomicBoolean();

		private List<Callable<?>> callables;

		private CountDownLatch latch;
		private CyclicBarrier barrier;

		private boolean random;
		private AtomicInteger indexer = new AtomicInteger();
		private Random randomizer = new Random();

		public Performance(int thread) {
			this.executor = Executors.newFixedThreadPool(thread);
			this.callables = new ArrayList<Callable<?>>();
			this.barrier = new CyclicBarrier(thread + 1);
			this.latch = new CountDownLatch(thread);
		}

		public Performance task(Callable<?>... callables) {
			this.callables.addAll(Arrays.asList(callables));
			return this;
		}

		public Performance warmup(long warmup) {
			this.warmup = warmup;
			return this;
		}

		public Performance interval(long interval) {
			this.interval = interval;
			return this;
		}

		public Performance sequencial() {
			this.random = false;
			return this;
		}

		public Performance random() {
			this.random = true;
			return this;
		}

		public int[] test(long timeout, TimeUnit timeunit) throws Exception {
			for (int i = 0; i < latch.getCount(); i++) {
				executor.execute(this);
			}
			barrier.await();

			if (warmup > 0) {
				latch.await(warmup, TimeUnit.MILLISECONDS);
				success.set(0);
				failed.set(0);
			}

			latch.await(timeout, timeunit);
			complete.set(true);

			latch.await();
			return new int[]{success.get(), failed.get()};
		}

		public void run() {
			try {
				barrier.await();
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}
			while (!complete.get()) {
				try {
					execute();
				} catch (Exception e) {
					failed.incrementAndGet();
					continue;
				}
				success.incrementAndGet();
				if (interval > 0) {
					try {
						Thread.sleep(interval);
					} catch (InterruptedException e) {
						// ignore
					}
				}
			}
			latch.countDown();
		}

		protected void execute() throws Exception {
			callables.get(random ? randomizer.nextInt(callables.size()) : indexer.incrementAndGet() % callables.size()).call();
		}
	}
}
